﻿/*

The MIT License

Copyright (c) 2010 Cartesian Analytics, Inc. 

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

*/

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.IO;
using Pipra.Gis.Transformation;
using Pipra.Math;
using Pipra.Math.Geometry;
using Pipra.Uom;
using Pipra.Uom.Core;
using Pipra.Uom.Imperial;
using Pipra.Uom.Si;

namespace Pipra.Gis.Ogc
{
    /// <summary>
    /// Serializes GIS entities to WKT format.
    /// </summary>
    public class WktSerializer
    {

        private static IAuthorityTag GetAuthorityTag(object entity)
        {
            return entity is IAuthorityBound ? (entity as IAuthorityBound).Authority : null;
        }

        private static string GetName(object entity)
        {
            return entity is INamedEntity ? (entity as INamedEntity).Name : null;
        }

        private static UomGraph GenerateDefaultUomGraph()
        {
            return GenerateDefaultUomGraph(new UomComparer());
        }

        private static UomGraph GenerateDefaultUomGraph(UomComparer cmp)
        {
            return new UomGraph(
                new IUomAuthority[]
                {
                    new CoreAngleAuthority(cmp),
                    new SiLengthAuthority(cmp),
                    new ImperialLengthAuthority(cmp)
                },
                cmp
            );
        }

        private static UomGraph _defaultUomGraph;

        private static UomGraph DefaultUomGraph
        {
            get { return _defaultUomGraph ?? (_defaultUomGraph = GenerateDefaultUomGraph()); }
        }

        private double GetReferenceFactor(IUom entity)
        {
            if(entity is OgcUnitBase)
            {
                return (entity as OgcUnitBase).Factor;
            }
            string refUnitName = null;
            if(entity.Type == "length")
            {
                refUnitName = "metre";
            }else if(entity.Type == "angle")
            {
                refUnitName = "radian";
            }
            if (!String.IsNullOrEmpty(refUnitName))
            {
                IValueConverter<double> conv = _uomGraph.FindPath(entity.Type, entity.Name, refUnitName);
                if(null != conv)
                {
                    return conv.TransformValue(1.0);
                }
            }
            return Double.NaN;
        }


        private class WktWriter
        {
            private int _indent;
            private readonly TextWriter _writer;
            private readonly WktOptions _options;


            public WktWriter(TextWriter writer, int indent, WktOptions options)
            {
                _writer = writer;
                _indent = indent;
                _options = options;
            }

            private string StringRep(WktKeyword keyword)
            {
                switch (keyword)
                {
                    case WktKeyword.ParamMt:
                        return "Param_Mt";
                    case WktKeyword.InverseMt:
                        return "Inverse_Mt";
                    case WktKeyword.PrimeMeridian:
                        return "PrimeM";
                    case WktKeyword.VerticalDatum:
                        return "Vert_Datum";
                    case WktKeyword.LocalDatum:
                        return "Local_Datum";
                    case WktKeyword.ConcatMt:
                        return "Concat_Mt";
                    case WktKeyword.GeographicCs:
                        return "GeogCs";
                    case WktKeyword.ProjectedCs:
                        return "ProjCs";
                    case WktKeyword.GeocentricCs:
                        return "GeocCs";
                    case WktKeyword.VerticalCs:
                        return "Vert_Cs";
                    case WktKeyword.LocalCs:
                        return "Local_Cs";
                    case WktKeyword.FittedCs:
                        return "Fitted_Cs";
                    case WktKeyword.CompoundCs:
                        return "Compd_Cs";
                    default:
                        return keyword.ToString();
                }
            }

            public void Write(WktKeyword keyword)
            {
                _writer.Write(StringRep(keyword).ToUpperInvariant());
            }

            public void Write(Orientation orientation)
            {
                _writer.Write(orientation.ToString().ToUpperInvariant());
            }

            public void WriteOpenParen()
            {
                _writer.Write('[');
            }

            public void WriteCloseParen()
            {
                _writer.Write(']');
            }

            private void WriteQuote()
            {
                _writer.Write('\"');
            }

            public void WriteQuoted(string text)
            {
                WriteQuote();
                if (null != text)
                {
                    _writer.Write(text);
                }
                WriteQuote();
            }

            public void WriteComma()
            {
                _writer.Write(',');
            }

            private void WriteTab()
            {
                if (WktOptions.Pretty == (WktOptions.Pretty & _options))
                {
                    _writer.Write('\t');
                }
            }

            public void WriteNewline()
            {
                if(WktOptions.Pretty == (WktOptions.Pretty & _options))
                {
                    _writer.WriteLine();
                    for (int i = 0; i < _indent; i++)
                    {
                        WriteTab();
                    }
                }
            }

            public void WriteValue(object value)
            {
                value = value ?? String.Empty;

                bool isValue = value.GetType().IsValueType;
                bool isNumber = isValue
                    && !(
                        value is bool
                        || value is char
                        || value.GetType().IsEnum
                    )
                ;
                string textOut;
                if(isValue)
                {
                    if(value is double || value is float)
                    {
                        textOut = String.Format(CultureInfo.InvariantCulture, "{0:r}", value);
                    }else
                    {
                        textOut = String.Format(CultureInfo.InvariantCulture, "{0}", value);
                    }
                }else
                {
                    textOut = value.ToString();
                }

                if(isNumber)
                {
                    _writer.Write(textOut);
                }else
                {
                    WriteQuoted(textOut);
                }
            }

            public void Indent()
            {
                _indent++;
            }

            public void Unindent()
            {
                _indent--;
            }

        }

        private readonly WktOptions _options;
        private readonly UomGraph _uomGraph;
        /// <summary>
        /// Constructs a WKT serialized with the given options.
        /// </summary>
        /// <param name="options">The serialization options.</param>
        public WktSerializer(WktOptions options)
            :this(options,null) { }
        /// <summary>
        /// Constructs a WKT serialized with the given options and UoM graph.
        /// </summary>
        /// <param name="options">The serialization options.</param>
        /// <param name="uomGraph">The UoM graph used to properly format unit information.</param>
        public WktSerializer(WktOptions options, UomGraph uomGraph)
        {
            _options = options;
            _uomGraph = uomGraph ?? DefaultUomGraph;
        }

        private void Serialize(IAuthorityTag entity, WktWriter writer)
        {
            writer.Write(WktKeyword.Authority);
            writer.WriteOpenParen();
            writer.WriteQuoted(entity.Name);
            writer.WriteComma();
            writer.WriteQuoted(entity.Code);
            writer.WriteCloseParen();
        }

        private void Serialize(INamedParameter entity, WktWriter writer)
        {
            writer.Write(WktKeyword.Parameter);
            writer.WriteOpenParen();
            writer.WriteQuoted(entity.Name.Replace(' ','_'));
            writer.WriteComma();
            writer.WriteValue(entity.Value ?? String.Empty);
            writer.WriteCloseParen();
        }

        private void Serialize(IEnumerable<INamedParameter> entity, WktWriter writer)
        {
            IEnumerator<INamedParameter> paramEnum = entity.GetEnumerator();
            if (paramEnum.MoveNext())
            {
                do
                {
                    writer.WriteComma();
                    writer.WriteNewline();
                    Serialize(paramEnum.Current, writer);
                } while (paramEnum.MoveNext());
            }
        }
        
        private void Serialize(ICoordinateOperationInfo entity, WktWriter writer)
        {
            writer.Write(WktKeyword.ParamMt);
            writer.WriteOpenParen();
            writer.WriteQuoted((entity.Name ?? String.Empty).Replace(' ','_'));
            writer.Indent();
            Serialize(entity.GetParameters(), writer);
            writer.Unindent();
            writer.WriteNewline();
            writer.WriteCloseParen();
        }

        private void Serialize(ITransformation entity, WktWriter writer)
        {
            var info = entity as ICoordinateOperationInfo;
            if (null == info && entity.HasInverse)
            {
                info = entity.GetInverse() as ICoordinateOperationInfo;
                if (null != info)
                {
                    writer.Write(WktKeyword.InverseMt);
                    writer.WriteOpenParen();
                    writer.Indent();
                    writer.WriteNewline();
                    Serialize(info, writer);
                    writer.Unindent();
                    writer.WriteNewline();
                    writer.WriteCloseParen();
                    return;
                }
            }
            if (null != info)
            {
                Serialize(info, writer);
                return;
            }
            string name = GetName(entity) ?? entity.GetType().Name;
            Serialize(new OgcCoordinateOperationInfo(name, null) as ICoordinateOperationInfo, writer);
        }

        private void Serialize(ISpheroid<double> entity, WktWriter writer)
        {
            writer.Write(WktKeyword.Spheroid);
            writer.WriteOpenParen();
            writer.Indent();
            writer.WriteNewline();
            writer.WriteQuoted(GetName(entity));
            writer.WriteComma();
            writer.WriteValue(entity.A);
            writer.WriteComma();
            writer.WriteValue(entity.InvF);
            IAuthorityTag authTag = GetAuthorityTag(entity);
            if(null != authTag)
            {
                writer.WriteComma();
                writer.WriteNewline();
                Serialize(authTag, writer);
            }
            writer.Unindent();
            writer.WriteNewline();
            writer.WriteCloseParen();
        }

        private void Serialize(IPrimeMeridian entity, WktWriter writer)
        {
            writer.Write(WktKeyword.PrimeMeridian);
            writer.WriteOpenParen();
            writer.Indent();
            writer.WriteNewline();
            writer.WriteQuoted(GetName(entity));
            writer.WriteComma();
            writer.WriteValue(entity.Longitude);
            IAuthorityTag authTag = GetAuthorityTag(entity);
            if(null != authTag)
            {
                writer.WriteComma();
                writer.WriteNewline();
                Serialize(authTag, writer);
            }
            writer.Unindent();
            writer.WriteNewline();
            writer.WriteCloseParen();
        }

        private void Serialize(IUom entity, WktWriter writer)
        {
            writer.Write(WktKeyword.Unit);
            writer.WriteOpenParen();
            writer.Indent();
            writer.WriteNewline();
            
            writer.WriteQuoted(entity.Name);
            writer.WriteComma();
            //writer.WriteNewline();

            writer.WriteValue(GetReferenceFactor(entity));
            IAuthorityTag authTag = GetAuthorityTag(entity);
            if (null != authTag)
            {
                writer.WriteComma();
                writer.WriteNewline();
                Serialize(authTag, writer);
            }
            writer.Unindent();
            writer.WriteNewline();
            writer.WriteCloseParen();
        }

        private void Serialize(Helmert7Transformation helmert, WktWriter writer)
        {
            writer.Write(WktKeyword.ToWgs84);
            writer.WriteOpenParen();
            writer.WriteValue(helmert.D.X);
            writer.WriteComma();
            writer.WriteValue(helmert.D.Y);
            writer.WriteComma();
            writer.WriteValue(helmert.D.Z);
            writer.WriteComma();
            writer.WriteValue(helmert.R.X);
            writer.WriteComma();
            writer.WriteValue(helmert.R.Y);
            writer.WriteComma();
            writer.WriteValue(helmert.R.Z);
            writer.WriteComma();
            writer.WriteValue(helmert.M);
            writer.WriteCloseParen();
        }

        private void Serialize(IDatum entity, WktWriter writer)
        {
            WktKeyword keyword = WktKeyword.Datum;
            ISpheroid<double> spheroid = null;
            Helmert7Transformation toWgs84 = null;
            if(entity is IDatumLocal)
            {
                keyword = WktKeyword.LocalDatum;
            }else if(entity is IDatumVertical)
            {
                keyword = WktKeyword.VerticalDatum;
            }else{
                if(entity is IDatumGeodetic<double>)
                {
                    spheroid = (entity as IDatumGeodetic<double>).Spheroid;
                }
                if(entity is ITransformableToWgs84)
                {
                    toWgs84 = (entity as ITransformableToWgs84).PrimaryTransformation;
                }
            }
            IAuthorityTag authTag = GetAuthorityTag(entity);

            writer.Write(keyword);
            writer.WriteOpenParen();
            writer.Indent();
            writer.WriteNewline();
            writer.WriteQuoted(GetName(entity));
            if (keyword != WktKeyword.Datum)
            {
                writer.WriteComma();
                writer.WriteValue((int)entity.DatumType);
            }

            if(null != spheroid)
            {
                writer.WriteComma();
                writer.WriteNewline();
                Serialize(spheroid, writer);
            }
            if(null != toWgs84)
            {
                writer.WriteComma();
                writer.WriteNewline();
                Serialize(toWgs84, writer);
            }
            if(null != authTag)
            {
                writer.WriteComma();
                writer.WriteNewline();
                Serialize(authTag, writer);
            }
            writer.Unindent();
            writer.WriteNewline();
            writer.WriteCloseParen();
        }

        private void Serialize(IAxis entity, WktWriter writer)
        {
            writer.Write(WktKeyword.Axis);
            writer.WriteOpenParen();
            writer.WriteQuoted(entity.Name);
            writer.WriteComma();
            writer.Write(entity.Orientation);
            writer.WriteCloseParen();
        }

        private void Serialize(IEnumerable<ITransformation> entities, WktWriter writer)
        {
            writer.Write(WktKeyword.ConcatMt);
            writer.WriteOpenParen();
            writer.Indent();
            var enumerator = entities.GetEnumerator();
            if(enumerator.MoveNext())
            {
                writer.WriteNewline();
                Serialize(enumerator.Current, writer);
                while(enumerator.MoveNext())
                {
                    writer.WriteComma();
                    writer.WriteNewline();
                    Serialize(enumerator.Current, writer);
                }
            }
            writer.Unindent();
            writer.WriteNewline();
            writer.WriteCloseParen();
        }

        private void Serialize(ICrs entity, WktWriter writer)
        {
            WktKeyword keyword = WktKeyword.None;
            IDatum datum = null;
            IPrimeMeridian primeMeridian = null;
            IUom unit = null;
            IEnumerable<IAxis> axes = null;
            IEnumerable<ICrs> crss = null;
            IProjection projection = null;
            ITransformation coordOp = null;
            if (entity is ICrsGeographic)
            {
                var geog = entity as ICrsGeographic;
                keyword = WktKeyword.GeographicCs;
                datum = geog.Datum;
                primeMeridian = geog.Datum.PrimeMeridian;
                unit = geog.Unit;
                axes = geog.GetAxes();
            }
            else if (entity is ICrsGeocentric)
            {
                var geoc = entity as ICrsGeocentric;
                keyword = WktKeyword.GeocentricCs;
                datum = geoc.Datum;
                primeMeridian = geoc.Datum.PrimeMeridian;
                unit = geoc.Unit;
                axes = geoc.GetAxes();
            }
            else if (entity is ICrsProjected)
            {
                var proj = entity as ICrsProjected;
                keyword = WktKeyword.ProjectedCs;
                unit = proj.Unit;
                axes = proj.GetAxes();
                crss = new ICrs[] { proj.BaseCrs };
                projection = proj.Projection;
            }
            else if(entity is ICrsVertical)
            {
                var vert = entity as ICrsVertical;
                keyword = WktKeyword.VerticalCs;
                datum = vert.Datum;
                unit = vert.Unit;
                axes = new[] {vert.Axis};
            }
            else if (entity is ICrsLocal)
            {
                var local = entity as ICrsLocal;
                keyword = WktKeyword.LocalCs;
                datum = local.Datum;
                unit = local.Unit;
                axes = local.GetAxes();
            }
            else if (entity is ICrsFitted)
            {
                var fitted = entity as ICrsFitted;
                keyword = WktKeyword.FittedCs;
                coordOp = fitted.ToBaseOperation;
                crss = new[] {fitted.BaseCrs};
            }
            else if (entity is ICrsCompound)
            {
                var comp = entity as ICrsCompound;
                keyword = WktKeyword.CompoundCs;
                crss = comp;
            }
            if (WktKeyword.None == keyword)
            {
                return;
            }

            writer.Write(keyword);
            writer.WriteOpenParen();
            writer.Indent();
            writer.WriteNewline();
            writer.WriteQuoted(GetName(entity));
            
            if (null != coordOp)
            {
                writer.WriteComma();
                writer.WriteNewline();
                Serialize(coordOp, writer);
            }

            if (null != crss)
            {
                foreach (var crs in crss)
                {
                    writer.WriteComma();
                    writer.WriteNewline();
                    Serialize(crs, writer);
                }
            }

            if(null != projection)
            {
                writer.WriteComma();
                writer.WriteNewline();
                writer.Write(WktKeyword.Projection);
                writer.WriteOpenParen();
                writer.WriteQuoted((GetName(projection) ?? String.Empty).Replace(' ', '_'));
                var prjAuthTag = GetAuthorityTag(projection);
                if (null != prjAuthTag)
                {
                    writer.Indent();
                    writer.WriteComma();
                    writer.WriteNewline();
                    Serialize(prjAuthTag, writer);
                    writer.WriteNewline();
                    writer.Unindent();
                }
                writer.WriteCloseParen();
                IEnumerable<INamedParameter> namedParams;
                if (projection is ICoordinateOperationInfo)
                {
                    namedParams = (projection as ICoordinateOperationInfo).GetParameters();
                }
                else if (projection is IEnumerable<INamedParameter>)
                {
                    namedParams = projection as IEnumerable<INamedParameter>;
                }
                else
                {
                    namedParams = null;
                }
                if(null != namedParams)
                {
                    foreach (var namedParam in namedParams)
                    {
                        writer.WriteComma();
                        writer.WriteNewline();
                        Serialize(namedParam, writer);
                    }
                }
            }
            if(null != datum)
            {
                writer.WriteComma();
                writer.WriteNewline();
                Serialize(datum, writer);
                if (null != primeMeridian)
                {
                    writer.WriteComma();
                    writer.WriteNewline();
                    Serialize(primeMeridian, writer);
                }
            }
            if(null != unit)
            {
                writer.WriteComma();
                writer.WriteNewline();
                Serialize(unit, writer);
            }

            if(null != axes)
            {
                foreach(var axis in axes)
                {
                    writer.WriteComma();
                    writer.WriteNewline();
                    Serialize(axis, writer);
                }
            }

            var auth = GetAuthorityTag(entity);
            if (null != auth)
            {
                writer.WriteComma();
                writer.WriteNewline();
                Serialize(auth, writer);
            }

            writer.Unindent();
            writer.WriteNewline();
            writer.WriteCloseParen();
        }

        /// <summary>
        /// Serializes an <paramref name="entity"/> to a text stream as WKT.
        /// </summary>
        /// <param name="entity">The entity to serialize.</param>
        /// <param name="writer">The writer to receive the WKT.</param>
        public void Serialize(object entity, TextWriter writer)
        {
            if (null == writer)
            {
                throw new ArgumentNullException("writer");
            }
            Serialize(entity, new WktWriter(writer,0,_options));
        }

        /// <summary>
        /// Serializes an <paramref name="entity"/> to a string as WKT.
        /// </summary>
        /// <param name="entity">The entity to serialize.</param>
        /// <returns>A WKT string.</returns>
        public string Serialize(object entity)
        {
            var builder = new StringBuilder();
            using (var writer = new StringWriter(builder))
            {
                Serialize(entity, writer);
            }
            return builder.ToString();
        }

        private void Serialize(object entity, WktWriter writer)
        {
            if (null == entity)
            {
                throw new ArgumentNullException("entity");
            }
            if (entity is IAuthorityTag)
            {
                Serialize(entity as IAuthorityTag, writer);
            }
            else if(entity is INamedParameter)
            {
                Serialize(entity as INamedParameter, writer);
            }
            else if (entity is IEnumerable<ITransformation>)
            {
                Serialize(entity as IEnumerable<ITransformation>, writer);
            }
            else if(entity is ICoordinateOperationInfo)
            {
                Serialize(entity as ICoordinateOperationInfo, writer);
            }
            else if (entity is ITransformation)
            {
                Serialize(entity as ITransformation, writer);
            }
            else if(entity is ICrs)
            {
                Serialize(entity as ICrs, writer);
            }
            else if(entity is ISpheroid<double>)
            {
                Serialize(entity as ISpheroid<double>, writer);
            }
            else if(entity is IPrimeMeridian)
            {
                Serialize(entity as IPrimeMeridian, writer);
            }
            else if(entity is IUom)
            {
                Serialize(entity as IUom, writer);
            }
            else if(entity is IDatum)
            {
                Serialize(entity as IDatum, writer);
            }else if(entity is IAxis)
            {
                Serialize(entity as IAxis, writer);
            }
            else
            {
                throw new ArgumentException("type not supported", "entity");
            }
        }


    }
}
